

package client

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"go.etcd.io/etcd/client/pkg/v3/pathutil"
	kjson "sigs.k8s.io/json"
)

const (
	ErrorCodeKeyNotFound  = 100
	ErrorCodeTestFailed   = 101
	ErrorCodeNotFile      = 102
	ErrorCodeNotDir       = 104
	ErrorCodeNodeExist    = 105
	ErrorCodeRootROnly    = 107
	ErrorCodeDirNotEmpty  = 108
	ErrorCodeUnauthorized = 110

	ErrorCodePrevValueRequired = 201
	ErrorCodeTTLNaN            = 202
	ErrorCodeIndexNaN          = 203
	ErrorCodeInvalidField      = 209
	ErrorCodeInvalidForm       = 210

	ErrorCodeRaftInternal = 300
	ErrorCodeLeaderElect  = 301

	ErrorCodeWatcherCleared    = 400
	ErrorCodeEventIndexCleared = 401
)

type Error struct {
	Code    int    `json:"errorCode"`
	Message string `json:"message"`
	Cause   string `json:"cause"`
	Index   uint64 `json:"index"`
}

func (e Error) Error() string {
	return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
}

var (
	ErrInvalidJSON = errors.New("client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint")
	ErrEmptyBody   = errors.New("client: response body is empty")
)

// PreveExistType用于在设置
// 或删除节点时定义存在条件。
type PrevExistType string

const (
	PrevIgnore  = PrevExistType("")
	PrevExist   = PrevExistType("true")
	PrevNoExist = PrevExistType("false")
)

var (
	defaultV2KeysPrefix = "/v2/keys"
)

// NewKeysAPI构建一个KeysAPI，通过HTTP与etcd的键值
// API交互。
func NewKeysAPI(c Client) KeysAPI {
	return NewKeysAPIWithPrefix(c, defaultV2KeysPrefix)
}

// NewKeysAPIWithPrefix的作用类似于NewKeysAPI，但允许调用方
// 提供自定义的基本URL路径。这只适用于
// 非常罕见的情况。
func NewKeysAPIWithPrefix(c Client, p string) KeysAPI {
	return &httpKeysAPI{
		client: c,
		prefix: p,
	}
}

type KeysAPI interface {
	// 查询键值对数据
	Get(ctx context.Context, key string, opts *GetOptions) (*Response, error)

	// 添加新的键值对数据
	Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error)

	// 删除指定的键位对数据
	Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error)

	// Create是Set w/PrevExist=false的别名
	Create(ctx context.Context, key, value string) (*Response, error)

	// CreateInOrder用于在给定目录内以原子方式创建顺序键。
	CreateInOrder(ctx context.Context, dir, value string, opts *CreateInOrderOptions) (*Response, error)

	// Update是Set w/PrevExist=true 
	Update(ctx context.Context, key, value string) (*Response, error)

	// 添加 Watcher
	Watcher(key string, opts *WatcherOptions) Watcher
}

type WatcherOptions struct {
	// AfterIndex定义了观察者应该在其之后开始发出事件的索引。例如，如果提供的值为5，则第一个事件的索引将大于等于6。
	// 
	// 将AfterIndex设置为0（默认值）意味着观察者
	// 应该开始观察从当前
	// 索引开始的事件，不管是什么。
	AfterIndex uint64

	// Recursive指定观察者是否应发出在给定键空间的子级中发生的
	// 事件。如果将
	// 设置为false（默认），则事件将仅限于针对确切密钥发生的
	// 事件。
	Recursive bool
}

type CreateInOrderOptions struct {
	// TTL定义了一段时间，在此时间之后节点应该过期并且不再存在。忽略小于等于0的值。如果
	// 忽略了零值，则不能使用TTL将
	// 的TTL设置为0。
	TTL time.Duration
}

type SetOptions struct {
	// PrevValue指定要使设置操作成功，节点的当前值必须是什么。
	// 
	// 将此字段留空表示调用方希望
	// 忽略节点的当前值。这不能用于将节点的当前值与空字符串进行比较。
	// 
	// 如果Dir=true，则忽略PrevValue 
	PrevValue string

	// PrevIndex指示
	// 节点的当前修改索引必须是什么才能使设置操作成功。
	// 
	// 如果PrevIndex设置为0（默认），则不进行比较。
	PrevIndex uint64

	// PrevExist指定节点当前是否必须存在
	// （PrevExist）或不存在（PrevNoExist）。如果调用者不关心是否存在，请将PreveExist设置为PrevIgnore，或者干脆不设置它。
	PrevExist PrevExistType

	// TTL定义了一段时间，在此时间之后节点应该过期并且不再存在。忽略小于等于0的值。如果
	// 忽略了零值，则不能使用TTL将
	// 的TTL设置为0。
	TTL time.Duration

	// 刷新设置为true意味着可以更新TTL值
	// 而无需启动手表或更改节点值。刷新密钥时，不能提供
	// 值。
	Refresh bool

	// Dir指定是否应将此节点创建为目录。
	Dir bool

	// NoValueOnSuccess指定响应是否包含节点的当前值。
	// 如果设置，则响应仅在请求失败时包含当前值。
	NoValueOnSuccess bool
}

type GetOptions struct {
	// 递归定义是否应返回节点
	// 的所有子节点。
	Recursive bool

	// Sort指示服务器是否对节点进行排序。
	// 如果为true，则节点按字母顺序按
	// 中的键升序（A到z）排序。如果为false（默认值），则节点将
	// 不进行排序，并且所使用的排序不应被视为
	// 可预测。
	Sort bool

	// Quorum指定是否获取成员的Quorum中应用了
	// 的最新提交值，以确保外部
	// 一致性（或线性化）。
	Quorum bool
}

type DeleteOptions struct {
	// PrevValue指定要使删除操作成功，节点的当前值必须是什么。
	// 
	// 将此字段留空表示调用方希望
	// 忽略节点的当前值。这不能用于将节点的当前值与空字符串进行比较。
	PrevValue string

	// PrevIndex指示要使删除操作成功，
	// 节点的当前ModifiedIndex必须是什么。
	// 
	// 如果PrevIndex设置为0（默认），则不进行比较。
	PrevIndex uint64

	// 递归定义是否应删除节点
	// 的所有子节点。如果设置为true，则将删除由给定密钥标识的节点
	// 的所有子节点。如果未设置
	// 或显式设置为false，则只会删除单个节点。
	Recursive bool

	// Dir指定是否应将此节点作为目录删除。
	Dir bool
}

type Watcher interface {
	// 下一个块直到etcd事件发生，然后返回表示该事件的响应
	// 。Next的行为取决于用于构造观察程序的
	// 观察选项。Next的设计目的是反复调用
	// 每次阻塞，直到后续事件
	// 可用。
	// 
	// 如果所提供的上下文被取消，Next将返回非nil 
	// 错误。等待下一个
	// 事件（连接问题、反序列化失败等）时遇到的任何其他故障也将导致非零错误。
	Next(context.Context) (*Response, error)
}

type Response struct {
	// 操作是发生的操作的名称。可能的值
	// 包括获取、设置、删除、更新、创建、比较DSAP、
	// 比较删除和过期。
	Action string `json:"action"`

	// 节点表示相关etcd节点的状态。
	Node *Node `json:"node"`

	// PrevNode表示节点的上一个状态。PrevNode为非nil 
	// 仅当该节点在操作发生之前存在且操作
	// 导致该节点发生更改时才有效。
	PrevNode *Node `json:"prevNode"`

	// 索引保存生成响应时的集群级索引。
	// 此索引未绑定到此响应中包含的节点。
	Index uint64 `json:"-"`

	// ClusterID保存服务器报告的集群级别ID。这个
	// 对于不同的etcd集群应该是不同的。
	ClusterID string `json:"-"`
}

type Node struct {
	// 键表示此节点的唯一位置（例如“/foo/bar”）。
	Key string `json:"key"`

	// Dir报告节点是否描述目录。
	Dir bool `json:"dir,omitempty"`

	// 值是存储在此节点上的当前数据。如果此节点
	// 是目录，则值为空。
	Value string `json:"value"`

	// 只有当此节点是目录时，节点才会保存此节点的子节点。
	// 如果发出递归Get或Watch请求，则此片段将任意深入（子、孙、曾孙等）。
	Nodes Nodes `json:"nodes"`

	// CreatedIndex是创建此节点的etcd索引。
	CreatedIndex uint64 `json:"createdIndex"`

	// ModifiedIndex是上次修改此节点的etcd索引。
	ModifiedIndex uint64 `json:"modifiedIndex"`

	// Expiration是密钥的服务器端过期时间。
	Expiration *time.Time `json:"expiration,omitempty"`

	// TTL是密钥的生存时间，以秒为单位。
	TTL int64 `json:"ttl,omitempty"`
}

func (n *Node) String() string {
	return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d, TTL: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex, n.TTL)
}

// TTLDuration以时间形式返回节点的TTL。Duration对象
func (n *Node) TTLDuration() time.Duration {
	return time.Duration(n.TTL) * time.Second
}

type Nodes []*Node

// 用于排序的接口

func (ns Nodes) Len() int           { return len(ns) }
func (ns Nodes) Less(i, j int) bool { return ns[i].Key < ns[j].Key }
func (ns Nodes) Swap(i, j int)      { ns[i], ns[j] = ns[j], ns[i] }

type httpKeysAPI struct {
	client httpClient
	prefix string
}

func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions) (*Response, error) {
	act := &setAction{	// 创建 setAction
		Prefix: k.prefix,	// 前缀路径
		Key:    key,
		Value:  val,
	}

	if opts != nil {	// 在 setAction 中设置相应参数
		act.PrevValue = opts.PrevValue
		act.PrevIndex = opts.PrevIndex
		act.PrevExist = opts.PrevExist
		act.TTL = opts.TTL
		act.Refresh = opts.Refresh
		act.Dir = opts.Dir
		act.NoValueOnSuccess = opts.NoValueOnSuccess
	}

	doCtx := ctx
	if act.PrevExist == PrevNoExist {
		doCtx = context.WithValue(doCtx, &oneShotCtxValue, &oneShotCtxValue)
	}
	// 调用 httpClient.Do （）方法
	resp, body, err := k.client.Do(doCtx, act)
	if err != nil {
		return nil, err
	}

	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
}

func (k *httpKeysAPI) Create(ctx context.Context, key, val string) (*Response, error) {
	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevNoExist})
}

func (k *httpKeysAPI) CreateInOrder(ctx context.Context, dir, val string, opts *CreateInOrderOptions) (*Response, error) {
	act := &createInOrderAction{
		Prefix: k.prefix,
		Dir:    dir,
		Value:  val,
	}

	if opts != nil {
		act.TTL = opts.TTL
	}

	resp, body, err := k.client.Do(ctx, act)
	if err != nil {
		return nil, err
	}

	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
}

func (k *httpKeysAPI) Update(ctx context.Context, key, val string) (*Response, error) {
	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevExist})
}

func (k *httpKeysAPI) Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error) {
	act := &deleteAction{
		Prefix: k.prefix,
		Key:    key,
	}

	if opts != nil {
		act.PrevValue = opts.PrevValue
		act.PrevIndex = opts.PrevIndex
		act.Dir = opts.Dir
		act.Recursive = opts.Recursive
	}

	doCtx := context.WithValue(ctx, &oneShotCtxValue, &oneShotCtxValue)
	resp, body, err := k.client.Do(doCtx, act)
	if err != nil {
		return nil, err
	}

	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
}

func (k *httpKeysAPI) Get(ctx context.Context, key string, opts *GetOptions) (*Response, error) {
	act := &getAction{
		Prefix: k.prefix,
		Key:    key,
	}

	if opts != nil {
		act.Recursive = opts.Recursive
		act.Sorted = opts.Sort
		act.Quorum = opts.Quorum
	}

	resp, body, err := k.client.Do(ctx, act)
	if err != nil {
		return nil, err
	}

	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
}

func (k *httpKeysAPI) Watcher(key string, opts *WatcherOptions) Watcher {
	act := waitAction{
		Prefix: k.prefix,
		Key:    key,
	}

	if opts != nil {
		act.Recursive = opts.Recursive
		if opts.AfterIndex > 0 {
			act.WaitIndex = opts.AfterIndex + 1
		}
	}

	return &httpWatcher{
		client:   k.client,
		nextWait: act,
	}
}

type httpWatcher struct {
	client   httpClient
	nextWait waitAction
}

func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
	for {
		httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
		if err != nil {
			return nil, err
		}

		resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
		if err != nil {
			if err == ErrEmptyBody {
				continue
			}
			return nil, err
		}

		hw.nextWait.WaitIndex = resp.Node.ModifiedIndex + 1
		return resp, nil
	}
}

// v2KeysURL形成表示键位置的URL。
// endpoint参数表示etcd的基本URL 
// 服务器。前缀是从
// 提供的端点路径路由到密钥API 
// （通常为“/v2/keys”）根目录所需的路径。
func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
	// 我们手动将所有部件连接在一起。我们不能使用
	// 路径。加入，因为它不保留尾部斜杠。
	// 我们调用CanonicalURLPath来进一步清理路径。
	if prefix != "" && prefix[0] != '/' {
		prefix = "/" + prefix
	}
	if key != "" && key[0] != '/' {
		key = "/" + key
	}
	ep.Path = pathutil.CanonicalURLPath(ep.Path + prefix + key)
	return &ep
}

type getAction struct {
	Prefix    string
	Key       string
	Recursive bool
	Sorted    bool
	Quorum    bool
}

func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2KeysURL(ep, g.Prefix, g.Key)

	params := u.Query()
	params.Set("recursive", strconv.FormatBool(g.Recursive))
	params.Set("sorted", strconv.FormatBool(g.Sorted))
	params.Set("quorum", strconv.FormatBool(g.Quorum))
	u.RawQuery = params.Encode()

	req, _ := http.NewRequest("GET", u.String(), nil)
	return req
}

type waitAction struct {
	Prefix    string
	Key       string
	WaitIndex uint64
	Recursive bool
}

func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2KeysURL(ep, w.Prefix, w.Key)

	params := u.Query()
	params.Set("wait", "true")
	params.Set("waitIndex", strconv.FormatUint(w.WaitIndex, 10))
	params.Set("recursive", strconv.FormatBool(w.Recursive))
	u.RawQuery = params.Encode()

	req, _ := http.NewRequest("GET", u.String(), nil)
	return req
}

type setAction struct {
	Prefix           string
	Key              string
	Value            string
	PrevValue        string
	PrevIndex        uint64
	PrevExist        PrevExistType
	TTL              time.Duration
	Refresh          bool
	Dir              bool
	NoValueOnSuccess bool
}

func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2KeysURL(ep, a.Prefix, a.Key)

	params := u.Query()
	form := url.Values{}

	// 我们正在创建目录或设置密钥
	if a.Dir {
		params.Set("dir", strconv.FormatBool(a.Dir))
	} else {
		// 这些选项仅对设置密钥有效
		if a.PrevValue != "" {
			params.Set("prevValue", a.PrevValue)
		}
		form.Add("value", a.Value)
	}

	// 这些选项适用于设置密钥和创建目录
	if a.PrevIndex != 0 {
		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
	}
	if a.PrevExist != PrevIgnore {
		params.Set("prevExist", string(a.PrevExist))
	}
	if a.TTL > 0 {
		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
	}

	if a.Refresh {
		form.Add("refresh", "true")
	}
	if a.NoValueOnSuccess {
		params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
	}

	u.RawQuery = params.Encode()
	body := strings.NewReader(form.Encode())

	req, _ := http.NewRequest("PUT", u.String(), body)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	return req
}

type deleteAction struct {
	Prefix    string
	Key       string
	PrevValue string
	PrevIndex uint64
	Dir       bool
	Recursive bool
}

func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2KeysURL(ep, a.Prefix, a.Key)

	params := u.Query()
	if a.PrevValue != "" {
		params.Set("prevValue", a.PrevValue)
	}
	if a.PrevIndex != 0 {
		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
	}
	if a.Dir {
		params.Set("dir", "true")
	}
	if a.Recursive {
		params.Set("recursive", "true")
	}
	u.RawQuery = params.Encode()

	req, _ := http.NewRequest("DELETE", u.String(), nil)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	return req
}

type createInOrderAction struct {
	Prefix string
	Dir    string
	Value  string
	TTL    time.Duration
}

func (a *createInOrderAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2KeysURL(ep, a.Prefix, a.Dir)

	form := url.Values{}
	form.Add("value", a.Value)
	if a.TTL > 0 {
		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
	}
	body := strings.NewReader(form.Encode())

	req, _ := http.NewRequest("POST", u.String(), body)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	return req
}

func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
	switch code {
	case http.StatusOK, http.StatusCreated:
		if len(body) == 0 {
			return nil, ErrEmptyBody
		}
		res, err = unmarshalSuccessfulKeysResponse(header, body)
	default:
		err = unmarshalFailedKeysResponse(body)
	}
	return res, err
}

func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
	var res Response
	err := kjson.UnmarshalCaseSensitivePreserveInts(body, &res)
	if err != nil {
		return nil, ErrInvalidJSON
	}
	if header.Get("X-Etcd-Index") != "" {
		res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
		if err != nil {
			return nil, err
		}
	}
	res.ClusterID = header.Get("X-Etcd-Cluster-ID")
	return &res, nil
}

func unmarshalFailedKeysResponse(body []byte) error {
	var etcdErr Error
	if err := json.Unmarshal(body, &etcdErr); err != nil {
		return ErrInvalidJSON
	}
	return etcdErr
}
